
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2015 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 麦当苗儿 <zuojiazi.cn@gmail.com> <http://www.zjzit.cn>
// +----------------------------------------------------------------------
namespace Com;
use Com\WechatCrypt;
//支持在非ThinkPHP环境下使用
defined('NOW_TIME') || define('NOW_TIME', $_SERVER['REQUEST_TIME']);
defined('IS_GET')   || define('IS_GET',   $_SERVER['REQUEST_METHOD'] == 'GET');
class Wechat {
	/**
	 * 消息类型常量
	 */
	const MSG_TYPE_TEXT	   = 'text';
	const MSG_TYPE_IMAGE	  = 'image';
	const MSG_TYPE_VOICE	  = 'voice';
	const MSG_TYPE_VIDEO	  = 'video';
	const MSG_TYPE_SHORTVIDEO = 'shortvideo';
	const MSG_TYPE_LOCATION   = 'location';
	const MSG_TYPE_LINK	   = 'link';
	const MSG_TYPE_MUSIC	  = 'music';
	const MSG_TYPE_NEWS	   = 'news';
	const MSG_TYPE_EVENT	  = 'event';
	/**
	 * 事件类型常量
	 */
	const MSG_EVENT_SUBSCRIBE   = 'subscribe';
	const MSG_EVENT_UNSUBSCRIBE = 'unsubscribe';
	const MSG_EVENT_SCAN		= 'SCAN';
	const MSG_EVENT_LOCATION	= 'LOCATION';
	const MSG_EVENT_CLICK	   = 'CLICK';
	const MSG_EVENT_VIEW		= 'VIEW';
	/**
	 * 微信推送过来的数据
	 * @var array
	 */
	private $data = array();
	/**
	 * 微信TOKEN
	 * @var string
	 */
	private static $token = '';
	/**
	 * 微信APP_ID
	 * @var string
	 */
	private static $appId = '';
	/**
	 * 消息加密KEY
	 * @var string
	 */
	private static $encodingAESKey = '';
	/**
	 * 是否使用安全模式
	 * @var boolean
	 */
	private static $msgSafeMode = false;
	/**
	 * 构造方法，用于实例化微信SDK
	 * 自动回复消息时实例化该SDK
	 * @param string $token 微信后台填写的TOKEN
	 * @param string $appid 微信APPID (安全模式和兼容模式有效)
	 * @param string $key   消息加密KEY (EncodingAESKey)
	 */
	public function __construct($token, $appid = '', $key = ''){
		//设置安全模式
		if(isset($_GET['encrypt_type']) && $_GET['encrypt_type'] == 'aes'){
			self::$msgSafeMode = true;
		}
		//参数验证
		if(self::$msgSafeMode){
			if(empty($key) || empty($appid)){
				throw new \Exception('缺少参数EncodingAESKey或APP_ID！');
			}
			self::$appId		  = $appid;
			self::$encodingAESKey = $key;
		}
		//TOKEN验证
		if($token){
			self::auth($token) || exit;
			if(IS_GET){
				exit($_GET['echostr']);
			} else {
				self::$token = $token;
				$this->init();
			}
		} else {
			throw new \Exception('缺少参数TOKEN！');
		}
	}
	/**
	 * 初始化微信推送的数据
	 */
	private function init(){
		$xml  = file_get_contents("php://input");
		$data = self::xml2data($xml);
		//安全模式 或兼容模式
		if(self::$msgSafeMode){
			if(isset($data['MsgType'])){
				//兼容模式追加解密后的消息内容
				$data['Decrypt'] = self::extract($data['Encrypt']);
			} else { 
				//安全模式
				$data = self::extract($data['Encrypt']);
			}
		}
		$this->data = $data;
	}
	/**
	 * 获取微信推送的数据
	 * @return array 转换为数组后的数据
	 */
	public function request(){
		return $this->data;
	}
	/**
	 * * 响应微信发送的信息（自动回复）
	 * @param  array  $content 回复信息，文本信息为string类型
	 * @param  string $type	消息类型
	 */
	public function response($content, $type = self::MSG_TYPE_TEXT){
		/* 基础数据 */
		$data = array(
			'ToUserName'   => $this->data['FromUserName'],
			'FromUserName' => $this->data['ToUserName'],
			'CreateTime'   => NOW_TIME,
			'MsgType'	  => $type,
		);
		/* 按类型添加额外数据 */
		$content = call_user_func(array(self, $type), $content);
		if($type == self::MSG_TYPE_TEXT || $type == self::MSG_TYPE_NEWS){
			$data = array_merge($data, $content);
		} else {
			$data[ucfirst($type)] = $content;
		}
		//安全模式，加密消息内容
		if(self::$msgSafeMode){
			$data = self::generate($data);
		}
		/* 转换数据为XML */
		$xml = new \SimpleXMLElement('<xml></xml>');
		self::data2xml($xml, $data);
		exit($xml->asXML());
	}
	/**
	 * 回复文本消息
	 * @param  string $text   回复的文字
	 */
	public function replyText($text){
		return $this->response($text, self::MSG_TYPE_TEXT);
	}
	/**
	 * 回复图片消息
	 * @param  string $media_id 图片ID
	 */
	public function replyImage($media_id){
		return $this->response($media_id, self::MSG_TYPE_IMAGE);
	}
	/**
	 * 回复语音消息
	 * @param  string $media_id 音频ID
	 */
	public function replyVoice($media_id){
		return $this->response($media_id, self::MSG_TYPE_VOICE);
	}
	/**
	 * 回复视频消息
	 * @param  string $media_id	视频ID
	 * @param  string $title	   视频标题
	 * @param  string $discription 视频描述
	 */
	public function replyVideo($media_id, $title, $discription){
		return $this->response(func_get_args(), self::MSG_TYPE_VIDEO);
	}
	/**
	 * 回复音乐消息
	 * @param  string $title		  音乐标题
	 * @param  string $discription	音乐描述
	 * @param  string $musicurl	   音乐链接
	 * @param  string $hqmusicurl	 高品质音乐链接
	 * @param  string $thumb_media_id 缩略图ID
	 */
	public function replyMusic($title, $discription, $musicurl, $hqmusicurl, $thumb_media_id){
		return $this->response(func_get_args(), self::MSG_TYPE_MUSIC);
	}
	/**
	 * 回复图文消息，一个参数代表一条信息
	 * @param  array  $news   图文内容 [标题，描述，URL，缩略图]
	 * @param  array  $news1  图文内容 [标题，描述，URL，缩略图]
	 * @param  array  $news2  图文内容 [标题，描述，URL，缩略图]
	 *				...	 ...
	 * @param  array  $news9  图文内容 [标题，描述，URL，缩略图]
	 */
	public function replyNews($news, $news1, $news2, $news3){
		return $this->response(func_get_args(), self::MSG_TYPE_NEWS);
	}
	/**
	 * 回复一条图文消息
	 * @param  string $title	   文章标题
	 * @param  string $discription 文章简介
	 * @param  string $url		 文章连接
	 * @param  string $picurl	  文章缩略图
	 */
	public function replyNewsOnce($title, $discription, $url, $picurl){
		return $this->response(array(func_get_args()), self::MSG_TYPE_NEWS);
	}
	/**
	 * 数据XML编码
	 * @param  object $xml  XML对象
	 * @param  mixed  $data 数据
	 * @param  string $item 数字索引时的节点名称
	 * @return string
	 */
	protected static function data2xml($xml, $data, $item = 'item') {
		foreach ($data as $key => $value) {
			/* 指定默认的数字key */
			is_numeric($key) && $key = $item;
			/* 添加子元素 */
			if(is_array($value) || is_object($value)){
				$child = $xml->addChild($key);
				self::data2xml($child, $value, $item);
			} else {
				if(is_numeric($value)){
					$child = $xml->addChild($key, $value);
				} else {
					$child = $xml->addChild($key);
					$node  = dom_import_simplexml($child);
					$cdata = $node->ownerDocument->createCDATASection($value);
					$node->appendChild($cdata);
				}
			}
		}
	}
	/**
	 * XML数据解码
	 * @param  string $xml 原始XML字符串
	 * @return array	   解码后的数组
	 */
	protected static function xml2data($xml){
		$xml = new \SimpleXMLElement($xml);
		
		if(!$xml){
			throw new \Exception('非法XXML');
		}
		$data = array();
		foreach ($xml as $key => $value) {
			$data[$key] = strval($value);
		}
		return $data;
	}
	/**
	 * 对数据进行签名认证，确保是微信发送的数据
	 * @param  string $token 微信开放平台设置的TOKEN
	 * @return boolean	   true-签名正确，false-签名错误
	 */
	protected static function auth($token){
		/* 获取数据 */
		$data = array($_GET['timestamp'], $_GET['nonce'], $token);
		$sign = $_GET['signature'];
		
		/* 对数据进行字典排序 */
		sort($data, SORT_STRING);
		/* 生成签名 */
		$signature = sha1(implode($data));
		return $signature === $sign;
	}
	/**
	 * 构造文本信息
	 * @param  string $content 要回复的文本
	 */
	private static function text($content){
		$data['Content'] = $content;
		return $data;
	}
	/**
	 * 构造图片信息
	 * @param  integer $media 图片ID
	 */
	private static function image($media){
		$data['MediaId'] = $media;
		return $data;
	}
	/**
	 * 构造音频信息
	 * @param  integer $media 语音ID
	 */
	private static function voice($media){
		$data['MediaId'] = $media;
		return $data;
	}
	/**
	 * 构造视频信息
	 * @param  array $video 要回复的视频 [视频ID，标题，说明]
	 */
	private static function video($video){
		$data = array();
		list(
			$data['MediaId'],
			$data['Title'], 
			$data['Description'], 
		) = $video;
		return $data;
	}
	/**
	 * 构造音乐信息
	 * @param  array $music 要回复的音乐[标题，说明，链接，高品质链接，缩略图ID]
	 */
	private static function music($music){
		$data = array();
		list(
			$data['Title'], 
			$data['Description'], 
			$data['MusicUrl'], 
			$data['HQMusicUrl'],
			$data['ThumbMediaId'],
		) = $music;
		return $data;
	}
	/**
	 * 构造图文信息
	 * @param  array $news 要回复的图文内容
	 * [	
	 *	  0 => 第一条图文信息[标题，说明，图片链接，全文连接]，
	 *	  1 => 第二条图文信息[标题，说明，图片链接，全文连接]，
	 *	  2 => 第三条图文信息[标题，说明，图片链接，全文连接]， 
	 * ]
	 */
	private static function news($news){
		$articles = array();
		foreach ($news as $key => $value) {
			list(
				$articles[$key]['Title'],
				$articles[$key]['Description'],
				$articles[$key]['Url'],
				$articles[$key]['PicUrl']
			) = $value;
			if($key >= 9) break; //最多只允许10条图文信息
		}
		$data['ArticleCount'] = count($articles);
		$data['Articles']	 = $articles;
		return $data;
	}
	/**
	 * 验证并解密密文数据
	 * @param  string $encrypt 密文
	 * @return array		   解密后的数据
	 */
	private static function extract($encrypt){
		//验证数据签名
		$signature = self::sign($_GET['timestamp'], $_GET['nonce'], $encrypt);
		if($signature != $_GET['msg_signature']){
			throw new \Exception('数据签名错误！');
		}
		//消息解密对象
		$WechatCrypt = new WechatCrypt(self::$encodingAESKey, self::$appId);
		//解密得到回明文消息
		$decrypt = $WechatCrypt->decrypt($encrypt);
		
		//返回解密的数据
		return self::xml2data($decrypt);
	}
	/**
	 * 加密并生成密文消息数据
	 * @param  array $data 获取到的加密的消息数据
	 * @return array	   生成的加密消息结构
	 */
	private static function generate($data){
		/* 转换数据为XML */
		$xml = new \SimpleXMLElement('<xml></xml>');
		self::data2xml($xml, $data);
		$xml = $xml->asXML();
		//消息加密对象
		$WechatCrypt = new WechatCrypt(self::$encodingAESKey, self::$appId);
		//加密得到密文消息
		$encrypt = $WechatCrypt->encrypt($xml);
		//签名
		$nonce	 = mt_rand(0, 9999999999);
		$signature = self::sign(NOW_TIME, $nonce, $encrypt);
		/* 加密消息基础数据 */
		$data = array(
			'Encrypt'	  => $encrypt,
			'MsgSignature' => $signature,
			'TimeStamp'	=> NOW_TIME,
			'Nonce'		=> $nonce,
		);
		return $data;
	}
	/**
	 * 生成数据签名
	 * @param  string $timestamp 时间戳
	 * @param  string $nonce	 随机数
	 * @param  string $encrypt   被签名的数据
	 * @return string			SHA1签名
	 */
	private static function sign($timestamp, $nonce, $encrypt){
		$sign  = array(self::$token, $timestamp, $nonce, $encrypt);
		sort($sign, SORT_STRING);
		return sha1(implode($sign));
	}
}